/**
 * @author Tim Knip / http://www.floorplanner.com/ / tim at floorplanner.com
 */

THREE.ColladaLoader = function () {

    var COLLADA = null;
    var scene = null;
    var daeScene;

    var readyCallbackFunc = null;

    var sources = {};
    var images = {};
    var animations = {};
    var controllers = {};
    var geometries = {};
    var materials = {};
    var effects = {};
    var cameras = {};
    var lights = {};

    var animData;
    var visualScenes;
    var baseUrl;
    var morphs;
    var skins;

    var flip_uv = true;
    var preferredShading = THREE.SmoothShading;

    var options = {
        // Force Geometry to always be centered at the local origin of the
        // containing Mesh.
        centerGeometry: false,

        // Axis conversion is done for geometries, animations, and controllers.
        // If we ever pull cameras or lights out of the COLLADA file, they'll
        // need extra work.
        convertUpAxis: false,

        subdivideFaces: true,

        upAxis: 'Y',

        // For reflective or refractive materials we'll use this cubemap
        defaultEnvMap: null

    };

    var colladaUnit = 1.0;
    var colladaUp = 'Y';
    var upConversion = null;

    function load(url, readyCallback, progressCallback) {

        var length = 0;

        if (document.implementation && document.implementation.createDocument) {

            var request = new XMLHttpRequest();

            request.onreadystatechange = function () {

                if (request.readyState == 4) {

                    if (request.status == 0 || request.status == 200) {


                        if (request.responseXML) {

                            readyCallbackFunc = readyCallback;
                            parse(request.responseXML, undefined, url);

                        } else if (request.responseText) {

                            readyCallbackFunc = readyCallback;
                            var xmlParser = new DOMParser();
                            var responseXML = xmlParser.parseFromString(request.responseText, "application/xml");
                            parse(responseXML, undefined, url);

                        } else {

                            console.error("ColladaLoader: Empty or non-existing file (" + url + ")");

                        }

                    }

                } else if (request.readyState == 3) {

                    if (progressCallback) {

                        if (length == 0) {

                            length = request.getResponseHeader("Content-Length");

                        }

                        progressCallback({ total: length, loaded: request.responseText.length });

                    }

                }

            }

            request.open("GET", url, true);
            request.send(null);

        } else {

            alert("Don't know how to parse XML!");

        }

    };

    function parse(doc, callBack, url) {

        COLLADA = doc;
        callBack = callBack || readyCallbackFunc;

        if (url !== undefined) {

            var parts = url.split('/');
            parts.pop();
            baseUrl = (parts.length < 1 ? '.' : parts.join('/')) + '/';

        }

        parseAsset();
        setUpConversion();
        images = parseLib("//dae:library_images/dae:image", _Image, "image");
        materials = parseLib("//dae:library_materials/dae:material", Material, "material");
        effects = parseLib("//dae:library_effects/dae:effect", Effect, "effect");
        geometries = parseLib("//dae:library_geometries/dae:geometry", Geometry, "geometry");
        cameras = parseLib(".//dae:library_cameras/dae:camera", Camera, "camera");
        lights = parseLib(".//dae:library_lights/dae:light", Light, "light");
        controllers = parseLib("//dae:library_controllers/dae:controller", Controller, "controller");
        animations = parseLib("//dae:library_animations/dae:animation", Animation, "animation");
        visualScenes = parseLib(".//dae:library_visual_scenes/dae:visual_scene", VisualScene, "visual_scene");

        morphs = [];
        skins = [];

        daeScene = parseScene();
        scene = new THREE.Object3D();

        for (var i = 0; i < daeScene.nodes.length; i++) {

            scene.add(createSceneGraph(daeScene.nodes[i]));

        }

        // unit conversion
        scene.scale.multiplyScalar(colladaUnit);

        createAnimations();

        var result = {

            scene: scene,
            morphs: morphs,
            skins: skins,
            animations: animData,
            dae: {
                images: images,
                materials: materials,
                cameras: cameras,
                lights: lights,
                effects: effects,
                geometries: geometries,
                controllers: controllers,
                animations: animations,
                visualScenes: visualScenes,
                scene: daeScene
            }

        };

        if (callBack) {

            callBack(result);

        }

        return result;

    };

    function setPreferredShading(shading) {

        preferredShading = shading;

    };

    function parseAsset() {

        var elements = COLLADA.evaluate('//dae:asset', COLLADA, _nsResolver, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

        var element = elements.iterateNext();

        if (element && element.childNodes) {

            for (var i = 0; i < element.childNodes.length; i++) {

                var child = element.childNodes[i];

                switch (child.nodeName) {

                    case 'unit':

                        var meter = child.getAttribute('meter');

                        if (meter) {

                            colladaUnit = parseFloat(meter);

                        }

                        break;

                    case 'up_axis':

                        colladaUp = child.textContent.charAt(0);
                        break;

                }

            }

        }

    };

    function parseLib(q, classSpec, prefix) {

        var elements = COLLADA.evaluate(q, COLLADA, _nsResolver, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

        var lib = {};
        var element = elements.iterateNext();
        var i = 0;

        while (element) {

            var daeElement = (new classSpec()).parse(element);
            if (!daeElement.id || daeElement.id.length == 0) daeElement.id = prefix + (i++);
            lib[daeElement.id] = daeElement;

            element = elements.iterateNext();

        }

        return lib;

    };

    function parseScene() {

        var sceneElement = COLLADA.evaluate('.//dae:scene/dae:instance_visual_scene', COLLADA, _nsResolver, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null).iterateNext();

        if (sceneElement) {

            var url = sceneElement.getAttribute('url').replace(/^#/, '');
            return visualScenes[url.length > 0 ? url : 'visual_scene0'];

        } else {

            return null;

        }

    };

    function createAnimations() {

        animData = [];

        // fill in the keys
        recurseHierarchy(scene);

    };

    function recurseHierarchy(node) {

        var n = daeScene.getChildById(node.name, true),
			newData = null;

        if (n && n.keys) {

            newData = {
                fps: 60,
                hierarchy: [{
                    node: n,
                    keys: n.keys,
                    sids: n.sids
                }],
                node: node,
                name: 'animation_' + node.name,
                length: 0
            };

            animData.push(newData);

            for (var i = 0, il = n.keys.length; i < il; i++) {

                newData.length = Math.max(newData.length, n.keys[i].time);

            }

        } else {

            newData = {
                hierarchy: [{
                    keys: [],
                    sids: []
                }]
            }

        }

        for (var i = 0, il = node.children.length; i < il; i++) {

            var d = recurseHierarchy(node.children[i]);

            for (var j = 0, jl = d.hierarchy.length; j < jl; j++) {

                newData.hierarchy.push({
                    keys: [],
                    sids: []
                });

            }

        }

        return newData;

    };

    function calcAnimationBounds() {

        var start = 1000000;
        var end = -start;
        var frames = 0;

        for (var id in animations) {

            var animation = animations[id];

            for (var i = 0; i < animation.sampler.length; i++) {

                var sampler = animation.sampler[i];
                sampler.create();

                start = Math.min(start, sampler.startTime);
                end = Math.max(end, sampler.endTime);
                frames = Math.max(frames, sampler.input.length);

            }

        }

        return { start: start, end: end, frames: frames };

    };

    function createMorph(geometry, ctrl) {

        var morphCtrl = ctrl instanceof InstanceController ? controllers[ctrl.url] : ctrl;

        if (!morphCtrl || !morphCtrl.morph) {

            console.log("could not find morph controller!");
            return;

        }

        var morph = morphCtrl.morph;

        for (var i = 0; i < morph.targets.length; i++) {

            var target_id = morph.targets[i];
            var daeGeometry = geometries[target_id];

            if (!daeGeometry.mesh ||
				 !daeGeometry.mesh.primitives ||
				 !daeGeometry.mesh.primitives.length) {
                continue;
            }

            var target = daeGeometry.mesh.primitives[0].geometry;

            if (target.vertices.length === geometry.vertices.length) {

                geometry.morphTargets.push({ name: "target_1", vertices: target.vertices });

            }

        }

        geometry.morphTargets.push({ name: "target_Z", vertices: geometry.vertices });

    };

    function createSkin(geometry, ctrl, applyBindShape) {

        var skinCtrl = controllers[ctrl.url];

        if (!skinCtrl || !skinCtrl.skin) {

            console.log("could not find skin controller!");
            return;

        }

        if (!ctrl.skeleton || !ctrl.skeleton.length) {

            console.log("could not find the skeleton for the skin!");
            return;

        }

        var skin = skinCtrl.skin;
        var skeleton = daeScene.getChildById(ctrl.skeleton[0]);
        var hierarchy = [];

        applyBindShape = applyBindShape !== undefined ? applyBindShape : true;

        var bones = [];
        geometry.skinWeights = [];
        geometry.skinIndices = [];

        //createBones( geometry.bones, skin, hierarchy, skeleton, null, -1 );
        //createWeights( skin, geometry.bones, geometry.skinIndices, geometry.skinWeights );

        /*
		geometry.animation = {
			name: 'take_001',
			fps: 30,
			length: 2,
			JIT: true,
			hierarchy: hierarchy
		};
		*/

        if (applyBindShape) {

            for (var i = 0; i < geometry.vertices.length; i++) {

                geometry.vertices[i].applyMatrix4(skin.bindShapeMatrix);

            }

        }

    };

    function setupSkeleton(node, bones, frame, parent) {

        node.world = node.world || new THREE.Matrix4();
        node.world.copy(node.matrix);

        if (node.channels && node.channels.length) {

            var channel = node.channels[0];
            var m = channel.sampler.output[frame];

            if (m instanceof THREE.Matrix4) {

                node.world.copy(m);

            }

        }

        if (parent) {

            node.world.multiplyMatrices(parent, node.world);

        }

        bones.push(node);

        for (var i = 0; i < node.nodes.length; i++) {

            setupSkeleton(node.nodes[i], bones, frame, node.world);

        }

    };

    function setupSkinningMatrices(bones, skin) {

        // FIXME: this is dumb...

        for (var i = 0; i < bones.length; i++) {

            var bone = bones[i];
            var found = -1;

            if (bone.type != 'JOINT') continue;

            for (var j = 0; j < skin.joints.length; j++) {

                if (bone.sid == skin.joints[j]) {

                    found = j;
                    break;

                }

            }

            if (found >= 0) {

                var inv = skin.invBindMatrices[found];

                bone.invBindMatrix = inv;
                bone.skinningMatrix = new THREE.Matrix4();
                bone.skinningMatrix.multiplyMatrices(bone.world, inv); // (IBMi * JMi)

                bone.weights = [];

                for (var j = 0; j < skin.weights.length; j++) {

                    for (var k = 0; k < skin.weights[j].length; k++) {

                        var w = skin.weights[j][k];

                        if (w.joint == found) {

                            bone.weights.push(w);

                        }

                    }

                }

            } else {

                throw 'ColladaLoader: Could not find joint \'' + bone.sid + '\'.';

            }

        }

    };

    function applySkin(geometry, instanceCtrl, frame) {

        var skinController = controllers[instanceCtrl.url];

        frame = frame !== undefined ? frame : 40;

        if (!skinController || !skinController.skin) {

            console.log('ColladaLoader: Could not find skin controller.');
            return;

        }

        if (!instanceCtrl.skeleton || !instanceCtrl.skeleton.length) {

            console.log('ColladaLoader: Could not find the skeleton for the skin. ');
            return;

        }

        var animationBounds = calcAnimationBounds();
        var skeleton = daeScene.getChildById(instanceCtrl.skeleton[0], true) ||
					   daeScene.getChildBySid(instanceCtrl.skeleton[0], true);

        var i, j, w, vidx, weight;
        var v = new THREE.Vector3(), o, s;

        // move vertices to bind shape

        for (i = 0; i < geometry.vertices.length; i++) {

            geometry.vertices[i].applyMatrix4(skinController.skin.bindShapeMatrix);

        }

        // process animation, or simply pose the rig if no animation

        for (frame = 0; frame < animationBounds.frames; frame++) {

            var bones = [];
            var skinned = [];

            // zero skinned vertices

            for (i = 0; i < geometry.vertices.length; i++) {

                skinned.push(new THREE.Vector3());

            }

            // process the frame and setup the rig with a fresh
            // transform, possibly from the bone's animation channel(s)

            setupSkeleton(skeleton, bones, frame);
            setupSkinningMatrices(bones, skinController.skin);

            // skin 'm

            for (i = 0; i < bones.length; i++) {

                if (bones[i].type != 'JOINT') continue;

                for (j = 0; j < bones[i].weights.length; j++) {

                    w = bones[i].weights[j];
                    vidx = w.index;
                    weight = w.weight;

                    o = geometry.vertices[vidx];
                    s = skinned[vidx];

                    v.x = o.x;
                    v.y = o.y;
                    v.z = o.z;

                    v.applyMatrix4(bones[i].skinningMatrix);

                    s.x += (v.x * weight);
                    s.y += (v.y * weight);
                    s.z += (v.z * weight);

                }

            }

            geometry.morphTargets.push({ name: "target_" + frame, vertices: skinned });

        }

    };

    function createSceneGraph(node, parent) {

        var obj = new THREE.Object3D();
        var skinned = false;
        var skinController;
        var morphController;
        var i, j;

        // FIXME: controllers

        for (i = 0; i < node.controllers.length; i++) {

            var controller = controllers[node.controllers[i].url];

            switch (controller.type) {

                case 'skin':

                    if (geometries[controller.skin.source]) {

                        var inst_geom = new InstanceGeometry();

                        inst_geom.url = controller.skin.source;
                        inst_geom.instance_material = node.controllers[i].instance_material;

                        node.geometries.push(inst_geom);
                        skinned = true;
                        skinController = node.controllers[i];

                    } else if (controllers[controller.skin.source]) {

                        // urgh: controller can be chained
                        // handle the most basic case...

                        var second = controllers[controller.skin.source];
                        morphController = second;
                        //	skinController = node.controllers[i];

                        if (second.morph && geometries[second.morph.source]) {

                            var inst_geom = new InstanceGeometry();

                            inst_geom.url = second.morph.source;
                            inst_geom.instance_material = node.controllers[i].instance_material;

                            node.geometries.push(inst_geom);

                        }

                    }

                    break;

                case 'morph':

                    if (geometries[controller.morph.source]) {

                        var inst_geom = new InstanceGeometry();

                        inst_geom.url = controller.morph.source;
                        inst_geom.instance_material = node.controllers[i].instance_material;

                        node.geometries.push(inst_geom);
                        morphController = node.controllers[i];

                    }

                    console.log('ColladaLoader: Morph-controller partially supported.');

                default:
                    break;

            }

        }

        // geometries

        var double_sided_materials = {};

        for (i = 0; i < node.geometries.length; i++) {

            var instance_geometry = node.geometries[i];
            var instance_materials = instance_geometry.instance_material;
            var geometry = geometries[instance_geometry.url];
            var used_materials = {};
            var used_materials_array = [];
            var num_materials = 0;
            var first_material;

            if (geometry) {

                if (!geometry.mesh || !geometry.mesh.primitives)
                    continue;

                if (obj.name.length == 0) {

                    obj.name = geometry.id;

                }

                // collect used fx for this geometry-instance

                if (instance_materials) {

                    for (j = 0; j < instance_materials.length; j++) {

                        var instance_material = instance_materials[j];
                        var mat = materials[instance_material.target];
                        var effect_id = mat.instance_effect.url;
                        var shader = effects[effect_id].shader;
                        var material3js = shader.material;

                        if (geometry.doubleSided) {

                            if (!(instance_material.symbol in double_sided_materials)) {

                                var _copied_material = material3js.clone();
                                _copied_material.side = THREE.DoubleSide;
                                double_sided_materials[instance_material.symbol] = _copied_material;

                            }

                            material3js = double_sided_materials[instance_material.symbol];

                        }

                        material3js.opacity = !material3js.opacity ? 1 : material3js.opacity;
                        used_materials[instance_material.symbol] = num_materials;
                        used_materials_array.push(material3js);
                        first_material = material3js;
                        first_material.name = mat.name == null || mat.name === '' ? mat.id : mat.name;
                        num_materials++;

                    }

                }

                var mesh;
                var material = first_material || new THREE.MeshLambertMaterial({ color: 0xdddddd, shading: THREE.FlatShading, side: geometry.doubleSided ? THREE.DoubleSide : THREE.FrontSide });
                var geom = geometry.mesh.geometry3js;

                if (num_materials > 1) {

                    material = new THREE.MeshFaceMaterial(used_materials_array);

                    for (j = 0; j < geom.faces.length; j++) {

                        var face = geom.faces[j];
                        face.materialIndex = used_materials[face.daeMaterial]

                    }

                }

                if (skinController !== undefined) {

                    applySkin(geom, skinController);

                    material.morphTargets = true;

                    mesh = new THREE.SkinnedMesh(geom, material, false);
                    mesh.skeleton = skinController.skeleton;
                    mesh.skinController = controllers[skinController.url];
                    mesh.skinInstanceController = skinController;
                    mesh.name = 'skin_' + skins.length;

                    skins.push(mesh);

                } else if (morphController !== undefined) {

                    createMorph(geom, morphController);

                    material.morphTargets = true;

                    mesh = new THREE.Mesh(geom, material);
                    mesh.name = 'morph_' + morphs.length;

                    morphs.push(mesh);

                } else {

                    mesh = new THREE.Mesh(geom, material);
                    // mesh.geom.name = geometry.id;

                }

                node.geometries.length > 1 ? obj.add(mesh) : obj = mesh;

            }

        }

        for (i = 0; i < node.cameras.length; i++) {

            var instance_camera = node.cameras[i];
            var cparams = cameras[instance_camera.url];

            obj = new THREE.PerspectiveCamera(cparams.fov, parseFloat(cparams.aspect_ratio),
					parseFloat(cparams.znear), parseFloat(cparams.zfar));

        }

        for (i = 0; i < node.lights.length; i++) {

            var instance_light = node.lights[i];
            var lparams = lights[instance_light.url];

            if (lparams && lparams.technique) {

                var color = lparams.color.getHex();
                var intensity = lparams.intensity;
                var distance = 0;
                var angle = lparams.falloff_angle;
                var exponent; // Intentionally undefined, don't know what this is yet

                switch (lparams.technique) {

                    case 'directional':

                        obj = new THREE.DirectionalLight(color, intensity, distance);
                        break;

                    case 'point':

                        obj = new THREE.PointLight(color, intensity, distance);
                        break;

                    case 'spot':

                        obj = new THREE.SpotLight(color, intensity, distance, angle, exponent);
                        break;

                    case 'ambient':

                        obj = new THREE.AmbientLight(color);
                        break;

                }

            }

        }

        obj.name = node.name || node.id || "";
        obj.matrix = node.matrix;
        obj.matrix.decompose(obj.position, obj.quaternion, obj.scale);

        if (options.centerGeometry && obj.geometry) {

            var delta = THREE.GeometryUtils.center(obj.geometry);
            delta.multiply(obj.scale);
            delta.applyQuaternion(obj.quaternion);

            obj.position.sub(delta);

        }

        for (i = 0; i < node.nodes.length; i++) {

            obj.add(createSceneGraph(node.nodes[i], node));

        }

        return obj;

    };

    function getJointId(skin, id) {

        for (var i = 0; i < skin.joints.length; i++) {

            if (skin.joints[i] == id) {

                return i;

            }

        }

    };

    function getLibraryNode(id) {

        return COLLADA.evaluate('.//dae:library_nodes//dae:node[@id=\'' + id + '\']', COLLADA, _nsResolver, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null).iterateNext();

    };

    function getChannelsForNode(node) {

        var channels = [];
        var startTime = 1000000;
        var endTime = -1000000;

        for (var id in animations) {

            var animation = animations[id];

            for (var i = 0; i < animation.channel.length; i++) {

                var channel = animation.channel[i];
                var sampler = animation.sampler[i];
                var id = channel.target.split('/')[0];

                if (id == node.id) {

                    sampler.create();
                    channel.sampler = sampler;
                    startTime = Math.min(startTime, sampler.startTime);
                    endTime = Math.max(endTime, sampler.endTime);
                    channels.push(channel);

                }

            }

        }

        if (channels.length) {

            node.startTime = startTime;
            node.endTime = endTime;

        }

        return channels;

    };

    function calcFrameDuration(node) {

        var minT = 10000000;

        for (var i = 0; i < node.channels.length; i++) {

            var sampler = node.channels[i].sampler;

            for (var j = 0; j < sampler.input.length - 1; j++) {

                var t0 = sampler.input[j];
                var t1 = sampler.input[j + 1];
                minT = Math.min(minT, t1 - t0);

            }
        }

        return minT;

    };

    function calcMatrixAt(node, t) {

        var animated = {};

        var i, j;

        for (i = 0; i < node.channels.length; i++) {

            var channel = node.channels[i];
            animated[channel.sid] = channel;

        }

        var matrix = new THREE.Matrix4();

        for (i = 0; i < node.transforms.length; i++) {

            var transform = node.transforms[i];
            var channel = animated[transform.sid];

            if (channel !== undefined) {

                var sampler = channel.sampler;
                var value;

                for (j = 0; j < sampler.input.length - 1; j++) {

                    if (sampler.input[j + 1] > t) {

                        value = sampler.output[j];
                        //console.log(value.flatten)
                        break;

                    }

                }

                if (value !== undefined) {

                    if (value instanceof THREE.Matrix4) {

                        matrix.multiplyMatrices(matrix, value);

                    } else {

                        // FIXME: handle other types

                        matrix.multiplyMatrices(matrix, transform.matrix);

                    }

                } else {

                    matrix.multiplyMatrices(matrix, transform.matrix);

                }

            } else {

                matrix.multiplyMatrices(matrix, transform.matrix);

            }

        }

        return matrix;

    };

    function bakeAnimations(node) {

        if (node.channels && node.channels.length) {

            var keys = [],
				sids = [];

            for (var i = 0, il = node.channels.length; i < il; i++) {

                var channel = node.channels[i],
					fullSid = channel.fullSid,
					sampler = channel.sampler,
					input = sampler.input,
					transform = node.getTransformBySid(channel.sid),
					member;

                if (channel.arrIndices) {

                    member = [];

                    for (var j = 0, jl = channel.arrIndices.length; j < jl; j++) {

                        member[j] = getConvertedIndex(channel.arrIndices[j]);

                    }

                } else {

                    member = getConvertedMember(channel.member);

                }

                if (transform) {

                    if (sids.indexOf(fullSid) === -1) {

                        sids.push(fullSid);

                    }

                    for (var j = 0, jl = input.length; j < jl; j++) {

                        var time = input[j],
							data = sampler.getData(transform.type, j),
							key = findKey(keys, time);

                        if (!key) {

                            key = new Key(time);
                            var timeNdx = findTimeNdx(keys, time);
                            keys.splice(timeNdx == -1 ? keys.length : timeNdx, 0, key);

                        }

                        key.addTarget(fullSid, transform, member, data);

                    }

                } else {

                    console.log('Could not find transform "' + channel.sid + '" in node ' + node.id);

                }

            }

            // post process
            for (var i = 0; i < sids.length; i++) {

                var sid = sids[i];

                for (var j = 0; j < keys.length; j++) {

                    var key = keys[j];

                    if (!key.hasTarget(sid)) {

                        interpolateKeys(keys, key, j, sid);

                    }

                }

            }

            node.keys = keys;
            node.sids = sids;

        }

    };

    function findKey(keys, time) {

        var retVal = null;

        for (var i = 0, il = keys.length; i < il && retVal == null; i++) {

            var key = keys[i];

            if (key.time === time) {

                retVal = key;

            } else if (key.time > time) {

                break;

            }

        }

        return retVal;

    };

    function findTimeNdx(keys, time) {

        var ndx = -1;

        for (var i = 0, il = keys.length; i < il && ndx == -1; i++) {

            var key = keys[i];

            if (key.time >= time) {

                ndx = i;

            }

        }

        return ndx;

    };

    function interpolateKeys(keys, key, ndx, fullSid) {

        var prevKey = getPrevKeyWith(keys, fullSid, ndx ? ndx - 1 : 0),
			nextKey = getNextKeyWith(keys, fullSid, ndx + 1);

        if (prevKey && nextKey) {

            var scale = (key.time - prevKey.time) / (nextKey.time - prevKey.time),
				prevTarget = prevKey.getTarget(fullSid),
				nextData = nextKey.getTarget(fullSid).data,
				prevData = prevTarget.data,
				data;

            if (prevTarget.type === 'matrix') {

                data = prevData;

            } else if (prevData.length) {

                data = [];

                for (var i = 0; i < prevData.length; ++i) {

                    data[i] = prevData[i] + (nextData[i] - prevData[i]) * scale;

                }

            } else {

                data = prevData + (nextData - prevData) * scale;

            }

            key.addTarget(fullSid, prevTarget.transform, prevTarget.member, data);

        }

    };

    // Get next key with given sid

    function getNextKeyWith(keys, fullSid, ndx) {

        for (; ndx < keys.length; ndx++) {

            var key = keys[ndx];

            if (key.hasTarget(fullSid)) {

                return key;

            }

        }

        return null;

    };

    // Get previous key with given sid

    function getPrevKeyWith(keys, fullSid, ndx) {

        ndx = ndx >= 0 ? ndx : ndx + keys.length;

        for (; ndx >= 0; ndx--) {

            var key = keys[ndx];

            if (key.hasTarget(fullSid)) {

                return key;

            }

        }

        return null;

    };

    function _Image() {

        this.id = "";
        this.init_from = "";

    };

    _Image.prototype.parse = function (element) {

        this.id = element.getAttribute('id');

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];

            if (child.nodeName == 'init_from') {

                this.init_from = child.textContent;

            }

        }

        return this;

    };

    function Controller() {

        this.id = "";
        this.name = "";
        this.type = "";
        this.skin = null;
        this.morph = null;

    };

    Controller.prototype.parse = function (element) {

        this.id = element.getAttribute('id');
        this.name = element.getAttribute('name');
        this.type = "none";

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];

            switch (child.nodeName) {

                case 'skin':

                    this.skin = (new Skin()).parse(child);
                    this.type = child.nodeName;
                    break;

                case 'morph':

                    this.morph = (new Morph()).parse(child);
                    this.type = child.nodeName;
                    break;

                default:
                    break;

            }
        }

        return this;

    };

    function Morph() {

        this.method = null;
        this.source = null;
        this.targets = null;
        this.weights = null;

    };

    Morph.prototype.parse = function (element) {

        var sources = {};
        var inputs = [];
        var i;

        this.method = element.getAttribute('method');
        this.source = element.getAttribute('source').replace(/^#/, '');

        for (i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'source':

                    var source = (new Source()).parse(child);
                    sources[source.id] = source;
                    break;

                case 'targets':

                    inputs = this.parseInputs(child);
                    break;

                default:

                    console.log(child.nodeName);
                    break;

            }

        }

        for (i = 0; i < inputs.length; i++) {

            var input = inputs[i];
            var source = sources[input.source];

            switch (input.semantic) {

                case 'MORPH_TARGET':

                    this.targets = source.read();
                    break;

                case 'MORPH_WEIGHT':

                    this.weights = source.read();
                    break;

                default:
                    break;

            }
        }

        return this;

    };

    Morph.prototype.parseInputs = function (element) {

        var inputs = [];

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'input':

                    inputs.push((new Input()).parse(child));
                    break;

                default:
                    break;
            }
        }

        return inputs;

    };

    function Skin() {

        this.source = "";
        this.bindShapeMatrix = null;
        this.invBindMatrices = [];
        this.joints = [];
        this.weights = [];

    };

    Skin.prototype.parse = function (element) {

        var sources = {};
        var joints, weights;

        this.source = element.getAttribute('source').replace(/^#/, '');
        this.invBindMatrices = [];
        this.joints = [];
        this.weights = [];

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'bind_shape_matrix':

                    var f = _floats(child.textContent);
                    this.bindShapeMatrix = getConvertedMat4(f);
                    break;

                case 'source':

                    var src = new Source().parse(child);
                    sources[src.id] = src;
                    break;

                case 'joints':

                    joints = child;
                    break;

                case 'vertex_weights':

                    weights = child;
                    break;

                default:

                    console.log(child.nodeName);
                    break;

            }
        }

        this.parseJoints(joints, sources);
        this.parseWeights(weights, sources);

        return this;

    };

    Skin.prototype.parseJoints = function (element, sources) {

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'input':

                    var input = (new Input()).parse(child);
                    var source = sources[input.source];

                    if (input.semantic == 'JOINT') {

                        this.joints = source.read();

                    } else if (input.semantic == 'INV_BIND_MATRIX') {

                        this.invBindMatrices = source.read();

                    }

                    break;

                default:
                    break;
            }

        }

    };

    Skin.prototype.parseWeights = function (element, sources) {

        var v, vcount, inputs = [];

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'input':

                    inputs.push((new Input()).parse(child));
                    break;

                case 'v':

                    v = _ints(child.textContent);
                    break;

                case 'vcount':

                    vcount = _ints(child.textContent);
                    break;

                default:
                    break;

            }

        }

        var index = 0;

        for (var i = 0; i < vcount.length; i++) {

            var numBones = vcount[i];
            var vertex_weights = [];

            for (var j = 0; j < numBones; j++) {

                var influence = {};

                for (var k = 0; k < inputs.length; k++) {

                    var input = inputs[k];
                    var value = v[index + input.offset];

                    switch (input.semantic) {

                        case 'JOINT':

                            influence.joint = value;//this.joints[value];
                            break;

                        case 'WEIGHT':

                            influence.weight = sources[input.source].data[value];
                            break;

                        default:
                            break;

                    }

                }

                vertex_weights.push(influence);
                index += inputs.length;
            }

            for (var j = 0; j < vertex_weights.length; j++) {

                vertex_weights[j].index = i;

            }

            this.weights.push(vertex_weights);

        }

    };

    function VisualScene() {

        this.id = "";
        this.name = "";
        this.nodes = [];
        this.scene = new THREE.Object3D();

    };

    VisualScene.prototype.getChildById = function (id, recursive) {

        for (var i = 0; i < this.nodes.length; i++) {

            var node = this.nodes[i].getChildById(id, recursive);

            if (node) {

                return node;

            }

        }

        return null;

    };

    VisualScene.prototype.getChildBySid = function (sid, recursive) {

        for (var i = 0; i < this.nodes.length; i++) {

            var node = this.nodes[i].getChildBySid(sid, recursive);

            if (node) {

                return node;

            }

        }

        return null;

    };

    VisualScene.prototype.parse = function (element) {

        this.id = element.getAttribute('id');
        this.name = element.getAttribute('name');
        this.nodes = [];

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'node':

                    this.nodes.push((new Node()).parse(child));
                    break;

                default:
                    break;

            }

        }

        return this;

    };

    function Node() {

        this.id = "";
        this.name = "";
        this.sid = "";
        this.nodes = [];
        this.controllers = [];
        this.transforms = [];
        this.geometries = [];
        this.channels = [];
        this.matrix = new THREE.Matrix4();

    };

    Node.prototype.getChannelForTransform = function (transformSid) {

        for (var i = 0; i < this.channels.length; i++) {

            var channel = this.channels[i];
            var parts = channel.target.split('/');
            var id = parts.shift();
            var sid = parts.shift();
            var dotSyntax = (sid.indexOf(".") >= 0);
            var arrSyntax = (sid.indexOf("(") >= 0);
            var arrIndices;
            var member;

            if (dotSyntax) {

                parts = sid.split(".");
                sid = parts.shift();
                member = parts.shift();

            } else if (arrSyntax) {

                arrIndices = sid.split("(");
                sid = arrIndices.shift();

                for (var j = 0; j < arrIndices.length; j++) {

                    arrIndices[j] = parseInt(arrIndices[j].replace(/\)/, ''));

                }

            }

            if (sid == transformSid) {

                channel.info = { sid: sid, dotSyntax: dotSyntax, arrSyntax: arrSyntax, arrIndices: arrIndices };
                return channel;

            }

        }

        return null;

    };

    Node.prototype.getChildById = function (id, recursive) {

        if (this.id == id) {

            return this;

        }

        if (recursive) {

            for (var i = 0; i < this.nodes.length; i++) {

                var n = this.nodes[i].getChildById(id, recursive);

                if (n) {

                    return n;

                }

            }

        }

        return null;

    };

    Node.prototype.getChildBySid = function (sid, recursive) {

        if (this.sid == sid) {

            return this;

        }

        if (recursive) {

            for (var i = 0; i < this.nodes.length; i++) {

                var n = this.nodes[i].getChildBySid(sid, recursive);

                if (n) {

                    return n;

                }

            }
        }

        return null;

    };

    Node.prototype.getTransformBySid = function (sid) {

        for (var i = 0; i < this.transforms.length; i++) {

            if (this.transforms[i].sid == sid) return this.transforms[i];

        }

        return null;

    };

    Node.prototype.parse = function (element) {

        var url;

        this.id = element.getAttribute('id');
        this.sid = element.getAttribute('sid');
        this.name = element.getAttribute('name');
        this.type = element.getAttribute('type');

        this.type = this.type == 'JOINT' ? this.type : 'NODE';

        this.nodes = [];
        this.transforms = [];
        this.geometries = [];
        this.cameras = [];
        this.lights = [];
        this.controllers = [];
        this.matrix = new THREE.Matrix4();

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'node':

                    this.nodes.push((new Node()).parse(child));
                    break;

                case 'instance_camera':

                    this.cameras.push((new InstanceCamera()).parse(child));
                    break;

                case 'instance_controller':

                    this.controllers.push((new InstanceController()).parse(child));
                    break;

                case 'instance_geometry':

                    this.geometries.push((new InstanceGeometry()).parse(child));
                    break;

                case 'instance_light':

                    this.lights.push((new InstanceLight()).parse(child));
                    break;

                case 'instance_node':

                    url = child.getAttribute('url').replace(/^#/, '');
                    var iNode = getLibraryNode(url);

                    if (iNode) {

                        this.nodes.push((new Node()).parse(iNode));

                    }

                    break;

                case 'rotate':
                case 'translate':
                case 'scale':
                case 'matrix':
                case 'lookat':
                case 'skew':

                    this.transforms.push((new Transform()).parse(child));
                    break;

                case 'extra':
                    break;

                default:

                    console.log(child.nodeName);
                    break;

            }

        }

        this.channels = getChannelsForNode(this);
        bakeAnimations(this);

        this.updateMatrix();

        return this;

    };

    Node.prototype.updateMatrix = function () {

        this.matrix.identity();

        for (var i = 0; i < this.transforms.length; i++) {

            this.transforms[i].apply(this.matrix);

        }

    };

    function Transform() {

        this.sid = "";
        this.type = "";
        this.data = [];
        this.obj = null;

    };

    Transform.prototype.parse = function (element) {

        this.sid = element.getAttribute('sid');
        this.type = element.nodeName;
        this.data = _floats(element.textContent);
        this.convert();

        return this;

    };

    Transform.prototype.convert = function () {

        switch (this.type) {

            case 'matrix':

                this.obj = getConvertedMat4(this.data);
                break;

            case 'rotate':

                this.angle = THREE.Math.degToRad(this.data[3]);

            case 'translate':

                fixCoords(this.data, -1);
                this.obj = new THREE.Vector3(this.data[0], this.data[1], this.data[2]);
                break;

            case 'scale':

                fixCoords(this.data, 1);
                this.obj = new THREE.Vector3(this.data[0], this.data[1], this.data[2]);
                break;

            default:
                console.log('Can not convert Transform of type ' + this.type);
                break;

        }

    };

    Transform.prototype.apply = function () {

        var m1 = new THREE.Matrix4();

        return function (matrix) {

            switch (this.type) {

                case 'matrix':

                    matrix.multiply(this.obj);

                    break;

                case 'translate':

                    matrix.multiply(m1.makeTranslation(this.obj.x, this.obj.y, this.obj.z));

                    break;

                case 'rotate':

                    matrix.multiply(m1.makeRotationAxis(this.obj, this.angle));

                    break;

                case 'scale':

                    matrix.scale(this.obj);

                    break;

            }

        };

    }();

    Transform.prototype.update = function (data, member) {

        var members = ['X', 'Y', 'Z', 'ANGLE'];

        switch (this.type) {

            case 'matrix':

                if (!member) {

                    this.obj.copy(data);

                } else if (member.length === 1) {

                    switch (member[0]) {

                        case 0:

                            this.obj.n11 = data[0];
                            this.obj.n21 = data[1];
                            this.obj.n31 = data[2];
                            this.obj.n41 = data[3];

                            break;

                        case 1:

                            this.obj.n12 = data[0];
                            this.obj.n22 = data[1];
                            this.obj.n32 = data[2];
                            this.obj.n42 = data[3];

                            break;

                        case 2:

                            this.obj.n13 = data[0];
                            this.obj.n23 = data[1];
                            this.obj.n33 = data[2];
                            this.obj.n43 = data[3];

                            break;

                        case 3:

                            this.obj.n14 = data[0];
                            this.obj.n24 = data[1];
                            this.obj.n34 = data[2];
                            this.obj.n44 = data[3];

                            break;

                    }

                } else if (member.length === 2) {

                    var propName = 'n' + (member[0] + 1) + (member[1] + 1);
                    this.obj[propName] = data;

                } else {

                    console.log('Incorrect addressing of matrix in transform.');

                }

                break;

            case 'translate':
            case 'scale':

                if (Object.prototype.toString.call(member) === '[object Array]') {

                    member = members[member[0]];

                }

                switch (member) {

                    case 'X':

                        this.obj.x = data;
                        break;

                    case 'Y':

                        this.obj.y = data;
                        break;

                    case 'Z':

                        this.obj.z = data;
                        break;

                    default:

                        this.obj.x = data[0];
                        this.obj.y = data[1];
                        this.obj.z = data[2];
                        break;

                }

                break;

            case 'rotate':

                if (Object.prototype.toString.call(member) === '[object Array]') {

                    member = members[member[0]];

                }

                switch (member) {

                    case 'X':

                        this.obj.x = data;
                        break;

                    case 'Y':

                        this.obj.y = data;
                        break;

                    case 'Z':

                        this.obj.z = data;
                        break;

                    case 'ANGLE':

                        this.angle = THREE.Math.degToRad(data);
                        break;

                    default:

                        this.obj.x = data[0];
                        this.obj.y = data[1];
                        this.obj.z = data[2];
                        this.angle = THREE.Math.degToRad(data[3]);
                        break;

                }
                break;

        }

    };

    function InstanceController() {

        this.url = "";
        this.skeleton = [];
        this.instance_material = [];

    };

    InstanceController.prototype.parse = function (element) {

        this.url = element.getAttribute('url').replace(/^#/, '');
        this.skeleton = [];
        this.instance_material = [];

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType !== 1) continue;

            switch (child.nodeName) {

                case 'skeleton':

                    this.skeleton.push(child.textContent.replace(/^#/, ''));
                    break;

                case 'bind_material':

                    var instances = COLLADA.evaluate('.//dae:instance_material', child, _nsResolver, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

                    if (instances) {

                        var instance = instances.iterateNext();

                        while (instance) {

                            this.instance_material.push((new InstanceMaterial()).parse(instance));
                            instance = instances.iterateNext();

                        }

                    }

                    break;

                case 'extra':
                    break;

                default:
                    break;

            }
        }

        return this;

    };

    function InstanceMaterial() {

        this.symbol = "";
        this.target = "";

    };

    InstanceMaterial.prototype.parse = function (element) {

        this.symbol = element.getAttribute('symbol');
        this.target = element.getAttribute('target').replace(/^#/, '');
        return this;

    };

    function InstanceGeometry() {

        this.url = "";
        this.instance_material = [];

    };

    InstanceGeometry.prototype.parse = function (element) {

        this.url = element.getAttribute('url').replace(/^#/, '');
        this.instance_material = [];

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            if (child.nodeName == 'bind_material') {

                var instances = COLLADA.evaluate('.//dae:instance_material', child, _nsResolver, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

                if (instances) {

                    var instance = instances.iterateNext();

                    while (instance) {

                        this.instance_material.push((new InstanceMaterial()).parse(instance));
                        instance = instances.iterateNext();

                    }

                }

                break;

            }

        }

        return this;

    };

    function Geometry() {

        this.id = "";
        this.mesh = null;

    };

    Geometry.prototype.parse = function (element) {

        this.id = element.getAttribute('id');

        extractDoubleSided(this, element);

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];

            switch (child.nodeName) {

                case 'mesh':

                    this.mesh = (new Mesh(this)).parse(child);
                    break;

                case 'extra':

                    // console.log( child );
                    break;

                default:
                    break;
            }
        }

        return this;

    };

    function Mesh(geometry) {

        this.geometry = geometry.id;
        this.primitives = [];
        this.vertices = null;
        this.geometry3js = null;

    };

    Mesh.prototype.parse = function (element) {

        this.primitives = [];

        var i, j;

        for (i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];

            switch (child.nodeName) {

                case 'source':

                    _source(child);
                    break;

                case 'vertices':

                    this.vertices = (new Vertices()).parse(child);
                    break;

                case 'triangles':

                    this.primitives.push((new Triangles().parse(child)));
                    break;

                case 'polygons':

                    this.primitives.push((new Polygons().parse(child)));
                    break;

                case 'polylist':

                    this.primitives.push((new Polylist().parse(child)));
                    break;

                default:
                    break;

            }

        }

        this.geometry3js = new THREE.Geometry();

        var vertexData = sources[this.vertices.input['POSITION'].source].data;

        for (i = 0; i < vertexData.length; i += 3) {

            this.geometry3js.vertices.push(getConvertedVec3(vertexData, i).clone());

        }

        for (i = 0; i < this.primitives.length; i++) {

            var primitive = this.primitives[i];
            primitive.setVertices(this.vertices);
            this.handlePrimitive(primitive, this.geometry3js);

        }

        this.geometry3js.computeCentroids();
        this.geometry3js.computeFaceNormals();

        if (this.geometry3js.calcNormals) {

            this.geometry3js.computeVertexNormals();
            delete this.geometry3js.calcNormals;

        }

        this.geometry3js.computeBoundingBox();

        return this;

    };

    Mesh.prototype.handlePrimitive = function (primitive, geom) {

        var j, k, pList = primitive.p, inputs = primitive.inputs;
        var input, index, idx32;
        var source, numParams;
        var vcIndex = 0, vcount = 3, maxOffset = 0;
        var texture_sets = [];

        for (j = 0; j < inputs.length; j++) {

            input = inputs[j];
            var offset = input.offset + 1;
            maxOffset = (maxOffset < offset) ? offset : maxOffset;

            switch (input.semantic) {

                case 'TEXCOORD':
                    texture_sets.push(input.set);
                    break;

            }

        }

        for (var pCount = 0; pCount < pList.length; ++pCount) {

            var p = pList[pCount], i = 0;

            while (i < p.length) {

                var vs = [];
                var ns = [];
                var ts = null;
                var cs = [];

                if (primitive.vcount) {

                    vcount = primitive.vcount.length ? primitive.vcount[vcIndex++] : primitive.vcount;

                } else {

                    vcount = p.length / maxOffset;

                }


                for (j = 0; j < vcount; j++) {

                    for (k = 0; k < inputs.length; k++) {

                        input = inputs[k];
                        source = sources[input.source];

                        index = p[i + (j * maxOffset) + input.offset];
                        numParams = source.accessor.params.length;
                        idx32 = index * numParams;

                        switch (input.semantic) {

                            case 'VERTEX':

                                vs.push(index);

                                break;

                            case 'NORMAL':

                                ns.push(getConvertedVec3(source.data, idx32));

                                break;

                            case 'TEXCOORD':

                                ts = ts || {};
                                if (ts[input.set] === undefined) ts[input.set] = [];
                                // invert the V
                                ts[input.set].push(new THREE.Vector2(source.data[idx32], source.data[idx32 + 1]));

                                break;

                            case 'COLOR':

                                cs.push(new THREE.Color().setRGB(source.data[idx32], source.data[idx32 + 1], source.data[idx32 + 2]));

                                break;

                            default:

                                break;

                        }

                    }

                }

                if (ns.length == 0) {

                    // check the vertices inputs
                    input = this.vertices.input.NORMAL;

                    if (input) {

                        source = sources[input.source];
                        numParams = source.accessor.params.length;

                        for (var ndx = 0, len = vs.length; ndx < len; ndx++) {

                            ns.push(getConvertedVec3(source.data, vs[ndx] * numParams));

                        }

                    } else {

                        geom.calcNormals = true;

                    }

                }

                if (!ts) {

                    ts = {};
                    // check the vertices inputs
                    input = this.vertices.input.TEXCOORD;

                    if (input) {

                        texture_sets.push(input.set);
                        source = sources[input.source];
                        numParams = source.accessor.params.length;

                        for (var ndx = 0, len = vs.length; ndx < len; ndx++) {

                            idx32 = vs[ndx] * numParams;
                            if (ts[input.set] === undefined) ts[input.set] = [];
                            // invert the V
                            ts[input.set].push(new THREE.Vector2(source.data[idx32], 1.0 - source.data[idx32 + 1]));

                        }

                    }

                }

                if (cs.length == 0) {

                    // check the vertices inputs
                    input = this.vertices.input.COLOR;

                    if (input) {

                        source = sources[input.source];
                        numParams = source.accessor.params.length;

                        for (var ndx = 0, len = vs.length; ndx < len; ndx++) {

                            idx32 = vs[ndx] * numParams;
                            cs.push(new THREE.Color().setRGB(source.data[idx32], source.data[idx32 + 1], source.data[idx32 + 2]));

                        }

                    }

                }

                var face = null, faces = [], uv, uvArr;

                if (vcount === 3) {

                    faces.push(new THREE.Face3(vs[0], vs[1], vs[2], ns, cs.length ? cs : new THREE.Color()));

                } else if (vcount === 4) {
                    faces.push(new THREE.Face4(vs[0], vs[1], vs[2], vs[3], ns, cs.length ? cs : new THREE.Color()));

                } else if (vcount > 4 && options.subdivideFaces) {

                    var clr = cs.length ? cs : new THREE.Color(),
						vec1, vec2, vec3, v1, v2, norm;

                    // subdivide into multiple Face3s

                    for (k = 1; k < vcount - 1;) {

                        // FIXME: normals don't seem to be quite right

                        faces.push(new THREE.Face3(vs[0], vs[k], vs[k + 1], [ns[0], ns[k++], ns[k]], clr));

                    }

                }

                if (faces.length) {

                    for (var ndx = 0, len = faces.length; ndx < len; ndx++) {

                        face = faces[ndx];
                        face.daeMaterial = primitive.material;
                        geom.faces.push(face);

                        for (k = 0; k < texture_sets.length; k++) {

                            uv = ts[texture_sets[k]];

                            if (vcount > 4) {

                                // Grab the right UVs for the vertices in this face
                                uvArr = [uv[0], uv[ndx + 1], uv[ndx + 2]];

                            } else if (vcount === 4) {

                                uvArr = [uv[0], uv[1], uv[2], uv[3]];

                            } else {

                                uvArr = [uv[0], uv[1], uv[2]];

                            }

                            if (!geom.faceVertexUvs[k]) {

                                geom.faceVertexUvs[k] = [];

                            }

                            geom.faceVertexUvs[k].push(uvArr);

                        }

                    }

                } else {

                    console.log('dropped face with vcount ' + vcount + ' for geometry with id: ' + geom.id);

                }

                i += maxOffset * vcount;

            }
        }

    };

    function Polygons() {

        this.material = "";
        this.count = 0;
        this.inputs = [];
        this.vcount = null;
        this.p = [];
        this.geometry = new THREE.Geometry();

    };

    Polygons.prototype.setVertices = function (vertices) {

        for (var i = 0; i < this.inputs.length; i++) {

            if (this.inputs[i].source == vertices.id) {

                this.inputs[i].source = vertices.input['POSITION'].source;

            }

        }

    };

    Polygons.prototype.parse = function (element) {

        this.material = element.getAttribute('material');
        this.count = _attr_as_int(element, 'count', 0);

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];

            switch (child.nodeName) {

                case 'input':

                    this.inputs.push((new Input()).parse(element.childNodes[i]));
                    break;

                case 'vcount':

                    this.vcount = _ints(child.textContent);
                    break;

                case 'p':

                    this.p.push(_ints(child.textContent));
                    break;

                case 'ph':

                    console.warn('polygon holes not yet supported!');
                    break;

                default:
                    break;

            }

        }

        return this;

    };

    function Polylist() {

        Polygons.call(this);

        this.vcount = [];

    };

    Polylist.prototype = Object.create(Polygons.prototype);

    function Triangles() {

        Polygons.call(this);

        this.vcount = 3;

    };

    Triangles.prototype = Object.create(Polygons.prototype);

    function Accessor() {

        this.source = "";
        this.count = 0;
        this.stride = 0;
        this.params = [];

    };

    Accessor.prototype.parse = function (element) {

        this.params = [];
        this.source = element.getAttribute('source');
        this.count = _attr_as_int(element, 'count', 0);
        this.stride = _attr_as_int(element, 'stride', 0);

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];

            if (child.nodeName == 'param') {

                var param = {};
                param['name'] = child.getAttribute('name');
                param['type'] = child.getAttribute('type');
                this.params.push(param);

            }

        }

        return this;

    };

    function Vertices() {

        this.input = {};

    };

    Vertices.prototype.parse = function (element) {

        this.id = element.getAttribute('id');

        for (var i = 0; i < element.childNodes.length; i++) {

            if (element.childNodes[i].nodeName == 'input') {

                var input = (new Input()).parse(element.childNodes[i]);
                this.input[input.semantic] = input;

            }

        }

        return this;

    };

    function Input() {

        this.semantic = "";
        this.offset = 0;
        this.source = "";
        this.set = 0;

    };

    Input.prototype.parse = function (element) {

        this.semantic = element.getAttribute('semantic');
        this.source = element.getAttribute('source').replace(/^#/, '');
        this.set = _attr_as_int(element, 'set', -1);
        this.offset = _attr_as_int(element, 'offset', 0);

        if (this.semantic == 'TEXCOORD' && this.set < 0) {

            this.set = 0;

        }

        return this;

    };

    function Source(id) {

        this.id = id;
        this.type = null;

    };

    Source.prototype.parse = function (element) {

        this.id = element.getAttribute('id');

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];

            switch (child.nodeName) {

                case 'bool_array':

                    this.data = _bools(child.textContent);
                    this.type = child.nodeName;
                    break;

                case 'float_array':

                    this.data = _floats(child.textContent);
                    this.type = child.nodeName;
                    break;

                case 'int_array':

                    this.data = _ints(child.textContent);
                    this.type = child.nodeName;
                    break;

                case 'IDREF_array':
                case 'Name_array':

                    this.data = _strings(child.textContent);
                    this.type = child.nodeName;
                    break;

                case 'technique_common':

                    for (var j = 0; j < child.childNodes.length; j++) {

                        if (child.childNodes[j].nodeName == 'accessor') {

                            this.accessor = (new Accessor()).parse(child.childNodes[j]);
                            break;

                        }
                    }
                    break;

                default:
                    // console.log(child.nodeName);
                    break;

            }

        }

        return this;

    };

    Source.prototype.read = function () {

        var result = [];

        //for (var i = 0; i < this.accessor.params.length; i++) {

        var param = this.accessor.params[0];

        //console.log(param.name + " " + param.type);

        switch (param.type) {

            case 'IDREF':
            case 'Name': case 'name':
            case 'float':

                return this.data;

            case 'float4x4':

                for (var j = 0; j < this.data.length; j += 16) {

                    var s = this.data.slice(j, j + 16);
                    var m = getConvertedMat4(s);
                    result.push(m);
                }

                break;

            default:

                console.log('ColladaLoader: Source: Read dont know how to read ' + param.type + '.');
                break;

        }

        //}

        return result;

    };

    function Material() {

        this.id = "";
        this.name = "";
        this.instance_effect = null;

    };

    Material.prototype.parse = function (element) {

        this.id = element.getAttribute('id');
        this.name = element.getAttribute('name');

        for (var i = 0; i < element.childNodes.length; i++) {

            if (element.childNodes[i].nodeName == 'instance_effect') {

                this.instance_effect = (new InstanceEffect()).parse(element.childNodes[i]);
                break;

            }

        }

        return this;

    };

    function ColorOrTexture() {

        this.color = new THREE.Color();
        this.color.setRGB(Math.random(), Math.random(), Math.random());
        this.color.a = 1.0;

        this.texture = null;
        this.texcoord = null;
        this.texOpts = null;

    };

    ColorOrTexture.prototype.isColor = function () {

        return (this.texture == null);

    };

    ColorOrTexture.prototype.isTexture = function () {

        return (this.texture != null);

    };

    ColorOrTexture.prototype.parse = function (element) {

        if (element.nodeName == 'transparent') {
            this.opaque = element.getAttribute('opaque');
        }

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'color':

                    var rgba = _floats(child.textContent);
                    this.color = new THREE.Color();
                    this.color.setRGB(rgba[0], rgba[1], rgba[2]);
                    this.color.a = rgba[3];
                    break;

                case 'texture':

                    this.texture = child.getAttribute('texture');
                    this.texcoord = child.getAttribute('texcoord');
                    // Defaults from:
                    // https://collada.org/mediawiki/index.php/Maya_texture_placement_MAYA_extension
                    this.texOpts = {
                        offsetU: 0,
                        offsetV: 0,
                        repeatU: 1,
                        repeatV: 1,
                        wrapU: 1,
                        wrapV: 1,
                    };
                    this.parseTexture(child);
                    break;

                default:
                    break;

            }

        }

        return this;

    };

    ColorOrTexture.prototype.parseTexture = function (element) {

        if (!element.childNodes) return this;

        // This should be supported by Maya, 3dsMax, and MotionBuilder

        if (element.childNodes[1] && element.childNodes[1].nodeName === 'extra') {

            element = element.childNodes[1];

            if (element.childNodes[1] && element.childNodes[1].nodeName === 'technique') {

                element = element.childNodes[1];

            }

        }

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];

            switch (child.nodeName) {

                case 'offsetU':
                case 'offsetV':
                case 'repeatU':
                case 'repeatV':

                    this.texOpts[child.nodeName] = parseFloat(child.textContent);
                    break;

                case 'wrapU':
                case 'wrapV':

                    this.texOpts[child.nodeName] = parseInt(child.textContent);
                    break;

                default:
                    this.texOpts[child.nodeName] = child.textContent;
                    break;

            }

        }

        return this;

    };

    function Shader(type, effect) {

        this.type = type;
        this.effect = effect;
        this.material = null;

    };

    Shader.prototype.parse = function (element) {

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'ambient':
                case 'emission':
                case 'diffuse':
                case 'specular':
                case 'transparent':

                    this[child.nodeName] = (new ColorOrTexture()).parse(child);
                    break;

                case 'shininess':
                case 'reflectivity':
                case 'index_of_refraction':
                case 'transparency':

                    var f = evaluateXPath(child, './/dae:float');

                    if (f.length > 0)
                        this[child.nodeName] = parseFloat(f[0].textContent);

                    break;

                default:
                    break;

            }

        }

        this.create();
        return this;

    };

    Shader.prototype.create = function () {

        var props = {};

        var transparent = false;
        if (this['transparency'] !== undefined && this['transparent'] !== undefined) {
            // convert transparent color RBG to average value
            var transparentColor = this['transparent'];
            var transparencyLevel = (this.transparent.color.r +
										this.transparent.color.g +
										this.transparent.color.b)
										/ 3 * this.transparency;

            if (transparencyLevel > 0) {
                transparent = true;
                props['transparent'] = true;
                props['opacity'] = 1 - transparencyLevel;
            }
        }

        for (var prop in this) {

            switch (prop) {

                case 'ambient':
                case 'emission':
                case 'diffuse':
                case 'specular':

                    var cot = this[prop];

                    if (cot instanceof ColorOrTexture) {

                        if (cot.isTexture()) {

                            var samplerId = cot.texture;
                            var surfaceId = this.effect.sampler[samplerId];

                            if (surfaceId !== undefined && surfaceId.source !== undefined) {

                                var surface = this.effect.surface[surfaceId.source];
                                var image = images[surface.init_from];

                                if (image) {

                                    var texture = THREE.ImageUtils.loadTexture(baseUrl + image.init_from);
                                    texture.wrapS = cot.texOpts.wrapU ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
                                    texture.wrapT = cot.texOpts.wrapV ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
                                    texture.offset.x = cot.texOpts.offsetU;
                                    texture.offset.y = cot.texOpts.offsetV;
                                    texture.repeat.x = cot.texOpts.repeatU;
                                    texture.repeat.y = cot.texOpts.repeatV;
                                    props['map'] = texture;

                                    // Texture with baked lighting?
                                    if (prop === 'emission') props['emissive'] = 0xffffff;

                                }

                            }

                        } else if (prop === 'diffuse' || !transparent) {

                            if (prop === 'emission') {

                                props['emissive'] = cot.color.getHex();

                            } else {

                                props[prop] = cot.color.getHex();

                            }

                        }

                    }

                    break;

                case 'shininess':

                    props[prop] = this[prop];
                    break;

                case 'reflectivity':

                    props[prop] = this[prop];
                    if (props[prop] > 0.0) props['envMap'] = options.defaultEnvMap;
                    props['combine'] = THREE.MixOperation;	//mix regular shading with reflective component
                    break;

                case 'index_of_refraction':

                    props['refractionRatio'] = this[prop]; //TODO: "index_of_refraction" becomes "refractionRatio" in shader, but I'm not sure if the two are actually comparable
                    if (this[prop] !== 1.0) props['envMap'] = options.defaultEnvMap;
                    break;

                case 'transparency':
                    // gets figured out up top
                    break;

                default:
                    break;

            }

        }

        props['shading'] = preferredShading;
        props['side'] = this.effect.doubleSided ? THREE.DoubleSide : THREE.FrontSide;

        switch (this.type) {

            case 'constant':

                if (props.emissive != undefined) props.color = props.emissive;
                this.material = new THREE.MeshBasicMaterial(props);
                break;

            case 'phong':
            case 'blinn':

                if (props.diffuse != undefined) props.color = props.diffuse;
                this.material = new THREE.MeshPhongMaterial(props);
                break;

            case 'lambert':
            default:

                if (props.diffuse != undefined) props.color = props.diffuse;
                this.material = new THREE.MeshLambertMaterial(props);
                break;

        }

        return this.material;

    };

    function Surface(effect) {

        this.effect = effect;
        this.init_from = null;
        this.format = null;

    };

    Surface.prototype.parse = function (element) {

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'init_from':

                    this.init_from = child.textContent;
                    break;

                case 'format':

                    this.format = child.textContent;
                    break;

                default:

                    console.log("unhandled Surface prop: " + child.nodeName);
                    break;

            }

        }

        return this;

    };

    function Sampler2D(effect) {

        this.effect = effect;
        this.source = null;
        this.wrap_s = null;
        this.wrap_t = null;
        this.minfilter = null;
        this.magfilter = null;
        this.mipfilter = null;

    };

    Sampler2D.prototype.parse = function (element) {

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'source':

                    this.source = child.textContent;
                    break;

                case 'minfilter':

                    this.minfilter = child.textContent;
                    break;

                case 'magfilter':

                    this.magfilter = child.textContent;
                    break;

                case 'mipfilter':

                    this.mipfilter = child.textContent;
                    break;

                case 'wrap_s':

                    this.wrap_s = child.textContent;
                    break;

                case 'wrap_t':

                    this.wrap_t = child.textContent;
                    break;

                default:

                    console.log("unhandled Sampler2D prop: " + child.nodeName);
                    break;

            }

        }

        return this;

    };

    function Effect() {

        this.id = "";
        this.name = "";
        this.shader = null;
        this.surface = {};
        this.sampler = {};

    };

    Effect.prototype.create = function () {

        if (this.shader == null) {

            return null;

        }

    };

    Effect.prototype.parse = function (element) {

        this.id = element.getAttribute('id');
        this.name = element.getAttribute('name');

        extractDoubleSided(this, element);

        this.shader = null;

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'profile_COMMON':

                    this.parseTechnique(this.parseProfileCOMMON(child));
                    break;

                default:
                    break;

            }

        }

        return this;

    };

    Effect.prototype.parseNewparam = function (element) {

        var sid = element.getAttribute('sid');

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'surface':

                    this.surface[sid] = (new Surface(this)).parse(child);
                    break;

                case 'sampler2D':

                    this.sampler[sid] = (new Sampler2D(this)).parse(child);
                    break;

                case 'extra':

                    break;

                default:

                    console.log(child.nodeName);
                    break;

            }

        }

    };

    Effect.prototype.parseProfileCOMMON = function (element) {

        var technique;

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];

            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'profile_COMMON':

                    this.parseProfileCOMMON(child);
                    break;

                case 'technique':

                    technique = child;
                    break;

                case 'newparam':

                    this.parseNewparam(child);
                    break;

                case 'image':

                    var _image = (new _Image()).parse(child);
                    images[_image.id] = _image;
                    break;

                case 'extra':
                    break;

                default:

                    console.log(child.nodeName);
                    break;

            }

        }

        return technique;

    };

    Effect.prototype.parseTechnique = function (element) {

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'constant':
                case 'lambert':
                case 'blinn':
                case 'phong':

                    this.shader = (new Shader(child.nodeName, this)).parse(child);
                    break;

                default:
                    break;

            }

        }

    };

    function InstanceEffect() {

        this.url = "";

    };

    InstanceEffect.prototype.parse = function (element) {

        this.url = element.getAttribute('url').replace(/^#/, '');
        return this;

    };

    function Animation() {

        this.id = "";
        this.name = "";
        this.source = {};
        this.sampler = [];
        this.channel = [];

    };

    Animation.prototype.parse = function (element) {

        this.id = element.getAttribute('id');
        this.name = element.getAttribute('name');
        this.source = {};

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];

            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'animation':

                    var anim = (new Animation()).parse(child);

                    for (var src in anim.source) {

                        this.source[src] = anim.source[src];

                    }

                    for (var j = 0; j < anim.channel.length; j++) {

                        this.channel.push(anim.channel[j]);
                        this.sampler.push(anim.sampler[j]);

                    }

                    break;

                case 'source':

                    var src = (new Source()).parse(child);
                    this.source[src.id] = src;
                    break;

                case 'sampler':

                    this.sampler.push((new Sampler(this)).parse(child));
                    break;

                case 'channel':

                    this.channel.push((new Channel(this)).parse(child));
                    break;

                default:
                    break;

            }

        }

        return this;

    };

    function Channel(animation) {

        this.animation = animation;
        this.source = "";
        this.target = "";
        this.fullSid = null;
        this.sid = null;
        this.dotSyntax = null;
        this.arrSyntax = null;
        this.arrIndices = null;
        this.member = null;

    };

    Channel.prototype.parse = function (element) {

        this.source = element.getAttribute('source').replace(/^#/, '');
        this.target = element.getAttribute('target');

        var parts = this.target.split('/');

        var id = parts.shift();
        var sid = parts.shift();

        var dotSyntax = (sid.indexOf(".") >= 0);
        var arrSyntax = (sid.indexOf("(") >= 0);

        if (dotSyntax) {

            parts = sid.split(".");
            this.sid = parts.shift();
            this.member = parts.shift();

        } else if (arrSyntax) {

            var arrIndices = sid.split("(");
            this.sid = arrIndices.shift();

            for (var j = 0; j < arrIndices.length; j++) {

                arrIndices[j] = parseInt(arrIndices[j].replace(/\)/, ''));

            }

            this.arrIndices = arrIndices;

        } else {

            this.sid = sid;

        }

        this.fullSid = sid;
        this.dotSyntax = dotSyntax;
        this.arrSyntax = arrSyntax;

        return this;

    };

    function Sampler(animation) {

        this.id = "";
        this.animation = animation;
        this.inputs = [];
        this.input = null;
        this.output = null;
        this.strideOut = null;
        this.interpolation = null;
        this.startTime = null;
        this.endTime = null;
        this.duration = 0;

    };

    Sampler.prototype.parse = function (element) {

        this.id = element.getAttribute('id');
        this.inputs = [];

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'input':

                    this.inputs.push((new Input()).parse(child));
                    break;

                default:
                    break;

            }

        }

        return this;

    };

    Sampler.prototype.create = function () {

        for (var i = 0; i < this.inputs.length; i++) {

            var input = this.inputs[i];
            var source = this.animation.source[input.source];

            switch (input.semantic) {

                case 'INPUT':

                    this.input = source.read();
                    break;

                case 'OUTPUT':

                    this.output = source.read();
                    this.strideOut = source.accessor.stride;
                    break;

                case 'INTERPOLATION':

                    this.interpolation = source.read();
                    break;

                case 'IN_TANGENT':

                    break;

                case 'OUT_TANGENT':

                    break;

                default:

                    console.log(input.semantic);
                    break;

            }

        }

        this.startTime = 0;
        this.endTime = 0;
        this.duration = 0;

        if (this.input.length) {

            this.startTime = 100000000;
            this.endTime = -100000000;

            for (var i = 0; i < this.input.length; i++) {

                this.startTime = Math.min(this.startTime, this.input[i]);
                this.endTime = Math.max(this.endTime, this.input[i]);

            }

            this.duration = this.endTime - this.startTime;

        }

    };

    Sampler.prototype.getData = function (type, ndx) {

        var data;

        if (type === 'matrix' && this.strideOut === 16) {

            data = this.output[ndx];

        } else if (this.strideOut > 1) {

            data = [];
            ndx *= this.strideOut;

            for (var i = 0; i < this.strideOut; ++i) {

                data[i] = this.output[ndx + i];

            }

            if (this.strideOut === 3) {

                switch (type) {

                    case 'rotate':
                    case 'translate':

                        fixCoords(data, -1);
                        break;

                    case 'scale':

                        fixCoords(data, 1);
                        break;

                }

            } else if (this.strideOut === 4 && type === 'matrix') {

                fixCoords(data, -1);

            }

        } else {

            data = this.output[ndx];

        }

        return data;

    };

    function Key(time) {

        this.targets = [];
        this.time = time;

    };

    Key.prototype.addTarget = function (fullSid, transform, member, data) {

        this.targets.push({
            sid: fullSid,
            member: member,
            transform: transform,
            data: data
        });

    };

    Key.prototype.apply = function (opt_sid) {

        for (var i = 0; i < this.targets.length; ++i) {

            var target = this.targets[i];

            if (!opt_sid || target.sid === opt_sid) {

                target.transform.update(target.data, target.member);

            }

        }

    };

    Key.prototype.getTarget = function (fullSid) {

        for (var i = 0; i < this.targets.length; ++i) {

            if (this.targets[i].sid === fullSid) {

                return this.targets[i];

            }

        }

        return null;

    };

    Key.prototype.hasTarget = function (fullSid) {

        for (var i = 0; i < this.targets.length; ++i) {

            if (this.targets[i].sid === fullSid) {

                return true;

            }

        }

        return false;

    };

    // TODO: Currently only doing linear interpolation. Should support full COLLADA spec.
    Key.prototype.interpolate = function (nextKey, time) {

        for (var i = 0; i < this.targets.length; ++i) {

            var target = this.targets[i],
				nextTarget = nextKey.getTarget(target.sid),
				data;

            if (target.transform.type !== 'matrix' && nextTarget) {

                var scale = (time - this.time) / (nextKey.time - this.time),
					nextData = nextTarget.data,
					prevData = target.data;

                // check scale error

                if (scale < 0 || scale > 1) {

                    console.log("Key.interpolate: Warning! Scale out of bounds:" + scale);
                    scale = scale < 0 ? 0 : 1;

                }

                if (prevData.length) {

                    data = [];

                    for (var j = 0; j < prevData.length; ++j) {

                        data[j] = prevData[j] + (nextData[j] - prevData[j]) * scale;

                    }

                } else {

                    data = prevData + (nextData - prevData) * scale;

                }

            } else {

                data = target.data;

            }

            target.transform.update(data, target.member);

        }

    };

    // Camera
    function Camera() {

        this.id = "";
        this.name = "";
        this.technique = "";

    };

    Camera.prototype.parse = function (element) {

        this.id = element.getAttribute('id');
        this.name = element.getAttribute('name');

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'optics':

                    this.parseOptics(child);
                    break;

                default:
                    break;

            }

        }

        return this;

    };

    Camera.prototype.parseOptics = function (element) {

        for (var i = 0; i < element.childNodes.length; i++) {

            if (element.childNodes[i].nodeName == 'technique_common') {

                var technique = element.childNodes[i];

                for (var j = 0; j < technique.childNodes.length; j++) {

                    this.technique = technique.childNodes[j].nodeName;

                    if (this.technique == 'perspective') {

                        var perspective = technique.childNodes[j];

                        for (var k = 0; k < perspective.childNodes.length; k++) {

                            var param = perspective.childNodes[k];

                            switch (param.nodeName) {

                                case 'yfov':
                                    this.yfov = param.textContent;
                                    break;
                                case 'xfov':
                                    this.xfov = param.textContent;
                                    break;
                                case 'znear':
                                    this.znear = param.textContent;
                                    break;
                                case 'zfar':
                                    this.zfar = param.textContent;
                                    break;
                                case 'aspect_ratio':
                                    this.aspect_ratio = param.textContent;
                                    break;

                            }

                        }

                    } else if (this.technique == 'orthographic') {

                        var orthographic = technique.childNodes[j];

                        for (var k = 0; k < orthographic.childNodes.length; k++) {

                            var param = orthographic.childNodes[k];

                            switch (param.nodeName) {

                                case 'xmag':
                                    this.xmag = param.textContent;
                                    break;
                                case 'ymag':
                                    this.ymag = param.textContent;
                                    break;
                                case 'znear':
                                    this.znear = param.textContent;
                                    break;
                                case 'zfar':
                                    this.zfar = param.textContent;
                                    break;
                                case 'aspect_ratio':
                                    this.aspect_ratio = param.textContent;
                                    break;

                            }

                        }

                    }

                }

            }

        }

        return this;

    };

    function InstanceCamera() {

        this.url = "";

    };

    InstanceCamera.prototype.parse = function (element) {

        this.url = element.getAttribute('url').replace(/^#/, '');

        return this;

    };

    // Light

    function Light() {

        this.id = "";
        this.name = "";
        this.technique = "";

    };

    Light.prototype.parse = function (element) {

        this.id = element.getAttribute('id');
        this.name = element.getAttribute('name');

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];
            if (child.nodeType != 1) continue;

            switch (child.nodeName) {

                case 'technique_common':

                    this.parseCommon(child);
                    break;

                case 'technique':

                    this.parseTechnique(child);
                    break;

                default:
                    break;

            }

        }

        return this;

    };

    Light.prototype.parseCommon = function (element) {

        for (var i = 0; i < element.childNodes.length; i++) {

            switch (element.childNodes[i].nodeName) {

                case 'directional':
                case 'point':
                case 'spot':
                case 'ambient':

                    this.technique = element.childNodes[i].nodeName;

                    var light = element.childNodes[i];

                    for (var j = 0; j < light.childNodes.length; j++) {

                        var child = light.childNodes[j];

                        switch (child.nodeName) {

                            case 'color':

                                var rgba = _floats(child.textContent);
                                this.color = new THREE.Color(0);
                                this.color.setRGB(rgba[0], rgba[1], rgba[2]);
                                this.color.a = rgba[3];
                                break;

                            case 'falloff_angle':

                                this.falloff_angle = parseFloat(child.textContent);
                                break;
                        }

                    }

            }

        }

        return this;

    };

    Light.prototype.parseTechnique = function (element) {

        this.profile = element.getAttribute('profile');

        for (var i = 0; i < element.childNodes.length; i++) {

            var child = element.childNodes[i];

            switch (child.nodeName) {

                case 'intensity':

                    this.intensity = parseFloat(child.textContent);
                    break;

            }

        }

        return this;

    };

    function InstanceLight() {

        this.url = "";

    };

    InstanceLight.prototype.parse = function (element) {

        this.url = element.getAttribute('url').replace(/^#/, '');

        return this;

    };

    function _source(element) {

        var id = element.getAttribute('id');

        if (sources[id] != undefined) {

            return sources[id];

        }

        sources[id] = (new Source(id)).parse(element);
        return sources[id];

    };

    function _nsResolver(nsPrefix) {

        if (nsPrefix == "dae") {

            return "http://www.collada.org/2005/11/COLLADASchema";

        }

        return null;

    };

    function _bools(str) {

        var raw = _strings(str);
        var data = [];

        for (var i = 0, l = raw.length; i < l; i++) {

            data.push((raw[i] == 'true' || raw[i] == '1') ? true : false);

        }

        return data;

    };

    function _floats(str) {

        var raw = _strings(str);
        var data = [];

        for (var i = 0, l = raw.length; i < l; i++) {

            data.push(parseFloat(raw[i]));

        }

        return data;

    };

    function _ints(str) {

        var raw = _strings(str);
        var data = [];

        for (var i = 0, l = raw.length; i < l; i++) {

            data.push(parseInt(raw[i], 10));

        }

        return data;

    };

    function _strings(str) {

        return (str.length > 0) ? _trimString(str).split(/\s+/) : [];

    };

    function _trimString(str) {

        return str.replace(/^\s+/, "").replace(/\s+$/, "");

    };

    function _attr_as_float(element, name, defaultValue) {

        if (element.hasAttribute(name)) {

            return parseFloat(element.getAttribute(name));

        } else {

            return defaultValue;

        }

    };

    function _attr_as_int(element, name, defaultValue) {

        if (element.hasAttribute(name)) {

            return parseInt(element.getAttribute(name), 10);

        } else {

            return defaultValue;

        }

    };

    function _attr_as_string(element, name, defaultValue) {

        if (element.hasAttribute(name)) {

            return element.getAttribute(name);

        } else {

            return defaultValue;

        }

    };

    function _format_float(f, num) {

        if (f === undefined) {

            var s = '0.';

            while (s.length < num + 2) {

                s += '0';

            }

            return s;

        }

        num = num || 2;

        var parts = f.toString().split('.');
        parts[1] = parts.length > 1 ? parts[1].substr(0, num) : "0";

        while (parts[1].length < num) {

            parts[1] += '0';

        }

        return parts.join('.');

    };

    function evaluateXPath(node, query) {

        var instances = COLLADA.evaluate(query, node, _nsResolver, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

        var inst = instances.iterateNext();
        var result = [];

        while (inst) {

            result.push(inst);
            inst = instances.iterateNext();

        }

        return result;

    };

    function extractDoubleSided(obj, element) {

        obj.doubleSided = false;

        var node = COLLADA.evaluate('.//dae:extra//dae:double_sided', element, _nsResolver, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

        if (node) {

            node = node.iterateNext();

            if (node && parseInt(node.textContent, 10) === 1) {

                obj.doubleSided = true;

            }

        }

    };

    // Up axis conversion

    function setUpConversion() {

        if (!options.convertUpAxis || colladaUp === options.upAxis) {

            upConversion = null;

        } else {

            switch (colladaUp) {

                case 'X':

                    upConversion = options.upAxis === 'Y' ? 'XtoY' : 'XtoZ';
                    break;

                case 'Y':

                    upConversion = options.upAxis === 'X' ? 'YtoX' : 'YtoZ';
                    break;

                case 'Z':

                    upConversion = options.upAxis === 'X' ? 'ZtoX' : 'ZtoY';
                    break;

            }

        }

    };

    function fixCoords(data, sign) {

        if (!options.convertUpAxis || colladaUp === options.upAxis) {

            return;

        }

        switch (upConversion) {

            case 'XtoY':

                var tmp = data[0];
                data[0] = sign * data[1];
                data[1] = tmp;
                break;

            case 'XtoZ':

                var tmp = data[2];
                data[2] = data[1];
                data[1] = data[0];
                data[0] = tmp;
                break;

            case 'YtoX':

                var tmp = data[0];
                data[0] = data[1];
                data[1] = sign * tmp;
                break;

            case 'YtoZ':

                var tmp = data[1];
                data[1] = sign * data[2];
                data[2] = tmp;
                break;

            case 'ZtoX':

                var tmp = data[0];
                data[0] = data[1];
                data[1] = data[2];
                data[2] = tmp;
                break;

            case 'ZtoY':

                var tmp = data[1];
                data[1] = data[2];
                data[2] = sign * tmp;
                break;

        }

    };

    function getConvertedVec3(data, offset) {

        var arr = [data[offset], data[offset + 1], data[offset + 2]];
        fixCoords(arr, -1);
        return new THREE.Vector3(arr[0], arr[1], arr[2]);

    };

    function getConvertedMat4(data) {

        if (options.convertUpAxis) {

            // First fix rotation and scale

            // Columns first
            var arr = [data[0], data[4], data[8]];
            fixCoords(arr, -1);
            data[0] = arr[0];
            data[4] = arr[1];
            data[8] = arr[2];
            arr = [data[1], data[5], data[9]];
            fixCoords(arr, -1);
            data[1] = arr[0];
            data[5] = arr[1];
            data[9] = arr[2];
            arr = [data[2], data[6], data[10]];
            fixCoords(arr, -1);
            data[2] = arr[0];
            data[6] = arr[1];
            data[10] = arr[2];
            // Rows second
            arr = [data[0], data[1], data[2]];
            fixCoords(arr, -1);
            data[0] = arr[0];
            data[1] = arr[1];
            data[2] = arr[2];
            arr = [data[4], data[5], data[6]];
            fixCoords(arr, -1);
            data[4] = arr[0];
            data[5] = arr[1];
            data[6] = arr[2];
            arr = [data[8], data[9], data[10]];
            fixCoords(arr, -1);
            data[8] = arr[0];
            data[9] = arr[1];
            data[10] = arr[2];

            // Now fix translation
            arr = [data[3], data[7], data[11]];
            fixCoords(arr, -1);
            data[3] = arr[0];
            data[7] = arr[1];
            data[11] = arr[2];

        }

        return new THREE.Matrix4(
			data[0], data[1], data[2], data[3],
			data[4], data[5], data[6], data[7],
			data[8], data[9], data[10], data[11],
			data[12], data[13], data[14], data[15]
			);

    };

    function getConvertedIndex(index) {

        if (index > -1 && index < 3) {

            var members = ['X', 'Y', 'Z'],
				indices = { X: 0, Y: 1, Z: 2 };

            index = getConvertedMember(members[index]);
            index = indices[index];

        }

        return index;

    };

    function getConvertedMember(member) {

        if (options.convertUpAxis) {

            switch (member) {

                case 'X':

                    switch (upConversion) {

                        case 'XtoY':
                        case 'XtoZ':
                        case 'YtoX':

                            member = 'Y';
                            break;

                        case 'ZtoX':

                            member = 'Z';
                            break;

                    }

                    break;

                case 'Y':

                    switch (upConversion) {

                        case 'XtoY':
                        case 'YtoX':
                        case 'ZtoX':

                            member = 'X';
                            break;

                        case 'XtoZ':
                        case 'YtoZ':
                        case 'ZtoY':

                            member = 'Z';
                            break;

                    }

                    break;

                case 'Z':

                    switch (upConversion) {

                        case 'XtoZ':

                            member = 'X';
                            break;

                        case 'YtoZ':
                        case 'ZtoX':
                        case 'ZtoY':

                            member = 'Y';
                            break;

                    }

                    break;

            }

        }

        return member;

    };

    return {

        load: load,
        parse: parse,
        setPreferredShading: setPreferredShading,
        applySkin: applySkin,
        geometries: geometries,
        options: options

    };

};
